﻿// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

namespace umbraco.ACSExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Web;
    using System.Web.Security;
    using System.Xml;
    using System.Xml.Linq;
    using Microsoft.IdentityModel.Claims;
    using umbraco.BusinessLogic;
    using umbraco.cms.businesslogic.member;
    using umbraco.IO;

    public class CustomClaimsAuthenticationManager : ClaimsAuthenticationManager
    {
        private const string ClaimIssuerName = "UmbracoCAM";

        public override IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
        {
            if (incomingPrincipal.Identity.IsAuthenticated && incomingPrincipal.Identity.AuthenticationType == "Federation")
            {
                var ticketNumber = Guid.Empty;
                var identity = incomingPrincipal.Identity as IClaimsIdentity;
                var nameIdentifier = identity.Name;
                var identityProviderClaim = identity.Claims.FirstOrDefault(o => o.ClaimType == ACSExtensionsSettings.IdentityProviderClaimType);

                if (identityProviderClaim == null)
                {
                    throw new InvalidOperationException("The Identity Provider claim was not found in the provided identity.");
                }

                if (IsActivationRequest(resourceName, out ticketNumber))
                {
                    var memberUniqueId = ticketNumber;
                    var member = new Member(memberUniqueId);

                    if (member == null)
                    {
                        throw new InvalidOperationException("The member does not exist. Please contact your administrator.");
                    }

                    var memberIsActivated = member.GetGenericProperty<bool>(ACSExtensionsSettings.MemberIsActivatedPropertyAlias);
                    if (memberIsActivated)
                    {
                        throw new InvalidOperationException("The specified ticket number has already been activated. If this is your account, you can login now.");
                    }

                    // Update LoginName property with NameIdentifier
                    // Set IdentityProvider (generic) property
                    member.LoginName = nameIdentifier;
                    member.SetGenericProperty(ACSExtensionsSettings.MemberIdentityProviderPropertyAlias, identityProviderClaim.Value);
                    member.SetGenericProperty(ACSExtensionsSettings.MemberIsActivatedPropertyAlias, bool.TrueString);

                    member.Save();
                }

                if (ACSExtensionsSettings.IsSignInFromBusinessIdentityProvider(identity))
                {
                    // Ensure temporary member
                    Member member = null;

                    var tempUserName = ACSExtensionsSettings.IsTemporaryMember(nameIdentifier) ? nameIdentifier : ACSExtensionsSettings.TemporaryMemberPrefix + nameIdentifier;
                    var tempUser = Membership.GetUser(tempUserName);
                    if (tempUser == null)
                    {
                        var emailClaim = identity.Claims.FirstOrDefault(o => o.ClaimType == ClaimTypes.Email);
                        if (emailClaim == null)
                        {
                            throw new InvalidOperationException("The Email claim was not found in the provided identity.");
                        }

                        member = Member.MakeNew(tempUserName, emailClaim.Value, ACSExtensionsSettings.SocialMemberType, User.GetUser(0));
                        member.SetGenericProperty(ACSExtensionsSettings.MemberIdentityProviderPropertyAlias, identityProviderClaim.Value);
                        member.Save();
                    }
                    else
                    {
                        // Temporary user already exists (it was not removed yet)
                        member = new Member((int)tempUser.ProviderUserKey);
                    }

                    identity.Claims.Remove(identity.Claims.Single(o => o.ClaimType == ClaimTypes.Name));
                    identity.Claims.Add(new Claim(ClaimTypes.Name, tempUserName));
                    
                    // Load Policies
                    LoadAuthorizationPolicies(incomingPrincipal, identityProviderClaim.Value, tempUserName);
                }
            }

            return incomingPrincipal;
        }

        private static void LoadAuthorizationPolicies(IClaimsPrincipal claimsPrincipal, string identityProvider, string userName)
        {
            var policies = new Dictionary<MemberGroupConfigurationSection, Func<IClaimsPrincipal, bool>>();
            var policyReader = new PolicyReader();
            var policiesXmlPath = IOHelper.MapPath(ACSExtensionsSettings.IdentityProvidersMappingRulesPath, false);
            var policiesXmlDocument = XDocument.Load(policiesXmlPath);

            Expression<Func<IClaimsPrincipal, bool>> policyExpression;

            var identityProviderPolicies =
                policiesXmlDocument.Element("mappingRules")
                                   .Elements("identityProvider")
                                   .Single(x => x.Attribute("id").Value == identityProvider)
                                   .Elements("memberGroup");

            foreach (var policyNode in identityProviderPolicies)
            {
                // Initialize the policy cache
                var reader = XmlDictionaryReader.CreateDictionaryReader(new XmlTextReader(new StringReader(policyNode.ToString())));
                reader.MoveToContent();

                int memberGroup = int.Parse(reader.GetAttribute("id"), CultureInfo.InvariantCulture);

                policyExpression = policyReader.ReadPolicy(reader);

                // Compile the policy expression into a function
                Func<IClaimsPrincipal, bool> policy = policyExpression.Compile();

                // Insert the policy function into the policy cache
                policies[new MemberGroupConfigurationSection(memberGroup)] = policy;
            }

            // Remove all roles
            var currentMemberGroups = Roles.GetRolesForUser(userName);
            if (currentMemberGroups.Length > 0)
            {
                Roles.RemoveUserFromRoles(userName, currentMemberGroups);
            }

            // Assign roles
            foreach (var group in MemberGroup.GetAll)
            {
                if (policies.ContainsKey(new MemberGroupConfigurationSection(group.Id)) &&
                    policies[new MemberGroupConfigurationSection(group.Id)](claimsPrincipal))
                {
                    Roles.AddUserToRole(userName, group.Text);
                }
            }
        }

        private static bool IsActivationRequest(string resourceName, out Guid ticketNumber)
        {
            ticketNumber = resourceName.StartsWith(ACSExtensionsSettings.ActivationPage, StringComparison.OrdinalIgnoreCase) ?
                    new Guid(HttpContext.Current.Request.QueryString["ticketNumber"]) :
                    Guid.Empty;

            return ticketNumber != Guid.Empty;
        }
    }
}